﻿using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using System.Data.SqlClient;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Linq;
using System.Xml.Schema;
using AppConfigData;

namespace AppConfigDataProxy
{
    /// <summary>
    /// Class to access data about Application configs stored in database
    /// </summary>
    public class AppConfigDBProxy:IAppConfigDataProxy
    {
        private readonly AppConfigData.AppConfigDataClassesDataContext _dataBase;
        private readonly ResourceErrorMessage _resourceErrorMessage;

        public AppConfigDBProxy(string connection)
        {
            
            _resourceErrorMessage = new ResourceErrorMessage();
            if (connection != null)
                _dataBase = new AppConfigData.AppConfigDataClassesDataContext(connection);
            else
                _dataBase = new AppConfigData.AppConfigDataClassesDataContext();

            var exists = _dataBase.DatabaseExists();
            if (!exists)
                throw new Exception( ResourceErrorMessage.ErrorConnectionToDB );
            
        }

        private ResourceErrorMessage ErrorMessage
        {
            get { return _resourceErrorMessage; }
        }
        private AppConfigData.AppConfigDataClassesDataContext DataBase
        {
            get { return _dataBase; }
        }

        /// <summary>
        /// Get config information for given application in xml
        /// </summary>
        /// <param name="appid">Application id</param>
        /// <returns>Config information for given application in xml</returns>
        public AppInfo GetAppConfig(string appid)
        {
            AppInfo result;

            try
            {
                var config = DataBase.GetAppConfig(appid);

                //convert Xelement to XmlElement
                var configXmlElement = Helper.GetXmlElement(config);

                result = new AppInfo(appid, configXmlElement);
            }
            catch (SqlException e)
            {
                throw new System.ArgumentException(ResourceErrorMessage.InvalidAppId, "appid", e);
            }
            catch(Exception)
            {
                throw;
            }

            return result;
        }

        /// <summary>
        /// Get schema for current config version
        /// </summary>
        /// <returns>schema in xml</returns>
        public XmlElement GetSchema()
        {
            XmlElement result;
            try
            {
                var schemaXml = DataBase.GetSchema();
                result = Helper.GetXmlElement(schemaXml);
            }
            catch (SqlException e)
            {
                throw new System.InvalidOperationException(ResourceErrorMessage.InternalDBError, e);
            }

            return result;
        }

        /// <summary>
        /// Get schema for current config version
        /// </summary>
        /// <returns>xsd schema</returns>
        public XmlSchema GetXsdSchema()
        {
            XmlSchema result;

            var schemaXml = GetSchema();
            result = Helper.GetXmlSchema( schemaXml );

            return result;
        }

        /// <summary>
        /// Get complete config information for all applications.
        /// </summary>
        /// <returns>Array of config app information</returns>
        public AppInfo[] GetConfig()
        {
            AppInfo[] result;
            try
            {
                IQueryable<GetConfigResult> configs = DataBase.GetConfig();

                //convert IQueryable<GetConfigResult> to AppInfo[]
                result = configs
                    .Select( r => new AppInfo(r.AppId, Helper.GetXmlElement(r.AppXml) ))
                    .ToArray();
                
            }
            catch (SqlException e)
            {
                throw new System.InvalidOperationException(ResourceErrorMessage.InternalDBError, e);
            }

            return result;
        }

        /// <summary>
        /// Set Application Configuration for a specific Application
        /// </summary>
        /// <param name="appInfo">Containe appId and new app config for this app</param>
        /// <returns>Updated application config version</returns>
        public int SetAppConfig(AppInfo appInfo)
        {
            int result;

            try
            {

                //check is app exists 
                var version = DataBase.GetAppVersion(appInfo.AppId);
                if (!version.HasValue)
                {
                    throw new System.ArgumentException(ResourceErrorMessage.InvalidAppId, "appInfo.AppId");
                }

                //validate app config with schema
                var schema = GetXsdSchema();
                Helper.ValidateApp(appInfo, schema);
            
                //convert XmlElement to Xelement
                var configXElement = Helper.GetXElement(appInfo.Config);

                DataBase.SetAppConfig(appInfo.AppId, configXElement, true);
                
                //Get App version
                version = DataBase.GetAppVersion( appInfo.AppId );

                Debug.Assert(version.HasValue);
                result = version.Value;

            }
            catch (SqlException e)
            {
                throw new System.ArgumentException(ResourceErrorMessage.InvalidAppId, "appInfo.AppId", e);
            }
            catch (Exception)
            {
                throw;
            }

            return result;
        }

        /// <summary>
        /// Set configuration information for all applications
        /// </summary>
        /// <param name="appConfigs">Array of config information for all applications</param>
        /// <param name="schemaXml">xsd schema for config</param>
        /// <returns></returns>
        public int SetConfig(AppInfo[] appConfigs, XmlElement schemaXml)
        {
            int result;
            try
            {
                //Check is schema valid
                string errDesription;
                bool isSchemaValid = Helper.IsValidXmlSchema(schemaXml, out errDesription);
                if (isSchemaValid == false)
                {
                    string errMes = ResourceErrorMessage.InvalidSchema + errDesription;
                    throw new ArgumentException(errMes, "schemaXml");
                }

                //first run, config not contain app,and we only change the schema
                if (appConfigs != null)
                {
                    //Check if all appId in request equals appId in current Config
                    var apps = GetAppList();
                    if (apps != null)
                    {                        
                        //find difference between list of all app in request and, all app in current config
                        IEnumerable<string> diff = apps.Except(appConfigs.Select(appInfo => appInfo.AppId));
                        if (diff.Count() != 0)
                            throw new ArgumentException(ResourceErrorMessage.InvalidAppIdList, "appConfigs");
                    }
                    else
                        throw new ArgumentException(ResourceErrorMessage.InvalidFirstRunSetConfig, "appConfigs");

                    //Validate all app configs in requst with schema
                    var schema = Helper.GetXmlSchema(schemaXml);

                    foreach (var appInfo in appConfigs)
                    {
                        //validate app config with schema                    
                        Helper.ValidateApp(appInfo, schema);
                    }

                }

                DbTransaction transaction = null;
                try
                {
                    //begin Transaction
                    if (DataBase.Connection.State == ConnectionState.Closed)
                        DataBase.Connection.Open();

                    transaction = DataBase.Connection.BeginTransaction();
                    DataBase.Transaction = transaction;

                    //change schema
                    var element = Helper.GetXElement(schemaXml);
                    DataBase.SetSchema(element);
                    
                    if (appConfigs != null)
                    {
                        //add all app configs
                        foreach (AppInfo appInfo in appConfigs)
                        {
                            //convert XmlElement to Xelement
                            XElement configXElement = appInfo.Config.GetXElement();

                            DataBase.SetAppConfig(appInfo.AppId, configXElement, false);
                        }
                    }

                    //end Transaction
                    transaction.Commit();

                }
                catch (Exception )
                {
                    if (transaction != null)
                    {
                        transaction.Rollback();
                    }
                    throw;
                }
                finally
                {
                    // force close connection
                    if (DataBase.Connection.State != ConnectionState.Closed)
                    {
                        //DataBase.Connection.Close();
                    }
                }                

                //return current config version
                var version = DataBase.GetConfigVersion();
                Debug.Assert(version.HasValue);

                result = version.Value;

            }
            catch (SqlException e)
            {
                throw new System.InvalidOperationException(ResourceErrorMessage.InternalDBError, e);
            }
 
            return result;
        }

        /// <summary>
        /// Creates a new application (if provided appid is not already used)
        /// and either sets it’s config to xml provided 
        /// </summary>
        /// <param name="appInfo">Containe appId and config information</param>
        /// <returns>Updated app config version</returns>
        public int AddApp(AppInfo appInfo)
        {
            int result;

            try
            {
                //check is app exists 
                var appVersion = DataBase.GetAppVersion(appInfo.AppId);
                if (appVersion.HasValue)
                {
                    throw new System.ArgumentException(ResourceErrorMessage.AppIdAlreadyExists,
                        "appInfo.AppId");
                }

                //validate app config with schema
                var schema = GetXsdSchema();
                Helper.ValidateApp( appInfo, schema );


                //convert XmlElement to Xelement
                var configXElement = Helper.GetXElement(appInfo.Config);
                //add app
                DataBase.AddApp(appInfo.AppId, configXElement);

                //Get App version
                appVersion = DataBase.GetAppVersion(appInfo.AppId);
                Debug.Assert(appVersion.HasValue);
                result = appVersion.Value;
            }
            catch (SqlException e)
            {
                throw new System.InvalidOperationException(ResourceErrorMessage.InternalDBError, e);
            }
            return result;
        }

        /// <summary>
        /// Remove a specific application from config
        /// </summary>
        /// <param name="appid">Application id what need to be removed</param>
        public void RemoveApp(string appid)
        {
            try
            {
                //check is application with appId exists in current Config
                var appConfigs = GetAppList();
                if (appConfigs.Contains(appid) == false)
                    throw new ArgumentException(ResourceErrorMessage.AppIdDontExists);

                //remove from Config
                DataBase.RemoveApp(appid);

            }
            catch (SqlException e)
            {
                throw new System.InvalidOperationException(ResourceErrorMessage.InternalDBError, e);
            }
        }

        /// <summary>
        /// Rolls back application configuration to the requested version 
        /// </summary>
        /// <param name="appid">Application Id what want to roll back</param>
        /// <param name="requestedAppConfigVersion">version to which want to roll back</param>
        /// <returns>updated application config version</returns>
        public int RollBackApp(string appid, int requestedAppConfigVersion)
        {
            return RollBackApp(appid, (int?)requestedAppConfigVersion);
        }

        /// <summary>
        /// Rolls back application configuration to the previous version
        /// </summary>
        /// <param name="appid">Application Id what want to roll back</param>        
        /// <returns>updated application config version</returns>
        public int RollBackApp(string appid)
        {
            return RollBackApp(appid, null);
        }

        /// <summary>
        /// Rolls back configuration to the requested version
        /// </summary>
        /// <param name="requestedConfigVersion">version to which want to roll back</param>
        /// <returns>Update config version</returns>
        public int RollBack(int requestedConfigVersion)
        {
            return RollBack((int?)requestedConfigVersion);
        }

        /// <summary>
        /// Rolls back configuration to the previous version
        /// </summary>        
        /// <returns>Update config version</returns>
        public int RollBack()
        {
            return RollBack(null);
        }

        /// <summary>
        /// Get current version of the requested application config
        /// </summary>
        /// <param name="appId">Application id</param>
        /// <returns>Current application config version</returns>
        public int GetAppVersion(string appId)
        {
            int result;
            try
            {
                var appVersion = DataBase.GetAppVersion(appId);
                if (!appVersion.HasValue)
                {
                    throw new System.ArgumentException(ResourceErrorMessage.AppIdDontExists,
                        "appInfo.AppId");
                }
                result = appVersion.Value;
            }
            catch (SqlException e)
            {
                throw new System.InvalidOperationException(ResourceErrorMessage.InternalDBError, e);
            }

            return result;
        }

        /// <summary>
        /// Get current config version
        /// </summary>
        /// <returns>Current config version</returns>
        public int GetVersion()
        {
            int result;
            try
            {
                var configVersion = DataBase.GetConfigVersion();
                Debug.Assert(configVersion.HasValue);
     
                result = configVersion.Value;
            }
            catch (SqlException e)
            {
                throw new System.InvalidOperationException(ResourceErrorMessage.InternalDBError, e);
            }

            return result;
        }

        /// <summary>
        /// Get the list of all applications on current config version
        /// </summary>
        /// <returns>Array of application id</returns>
        public string[] GetAppList()
        {
            string[] result;
            try
            {
                IQueryable<GetAppListResult> appList = DataBase.GetAppList();
                //convert IQueryable<GetAppListResult> to AppInfo[]
                if (appList.Count() > 0)
                    result = appList
                        .Select(r => r.appId)
                        .ToArray();
                else                
                    result = null;                
            }
            catch (SqlException e)
            {
                throw new System.InvalidOperationException(ResourceErrorMessage.InternalDBError, e);
            }

            return result;
        }

        /// <summary>
        /// Delete all information about all Application and config
        /// from data without possibility to rollback
        /// </summary>
        public void ClearAll()
        {
            DataBase.CleanDB();
        }

        private int RollBackApp(string appid, int? requestedAppConfigVersion)
        {
            int result;
            try
            {
                //check is application with appId exists in current Config
                var appConfigs = GetAppList();
                if (!appConfigs.Contains(appid))
                    throw new ArgumentException(ResourceErrorMessage.AppIdDontExists);

                //check is rollback possible
                var appVersion = DataBase.GetAppVersion(appid);
                Debug.Assert(appVersion.HasValue);
                if (appVersion == 1 )
                    throw new InvalidOperationException(ResourceErrorMessage.InvalidRollbackAppOperation);

                //check is requested version is valid
                if (requestedAppConfigVersion.HasValue)
                {
                    if (requestedAppConfigVersion <= 0 || appVersion <= requestedAppConfigVersion)
                        throw new ArgumentOutOfRangeException("requestedAppConfigVersion",
                            ResourceErrorMessage.InvalidAppVersion);
                }

                //TODO: check(or resolve) incompatibles requsted version config with current schema

                //rollback app
                DataBase.RollBackApp(appid, requestedAppConfigVersion);

                //get version
                var updatedAppVersion = DataBase.GetAppVersion(appid);
                Debug.Assert(updatedAppVersion.HasValue);
                result = updatedAppVersion.Value;

                if (requestedAppConfigVersion.HasValue)
                    Debug.Assert(updatedAppVersion == requestedAppConfigVersion);
                else
                    Debug.Assert(updatedAppVersion == appVersion - 1);
                
            }
            catch (SqlException e)
            {
                throw new System.InvalidOperationException(ResourceErrorMessage.InternalDBError, e);
            }

            return result;
        }

        private int RollBack(int? requestedConfigVersion)
        {
            int result;
            try
            {

                //check is rollback possible
                var configVersion = DataBase.GetConfigVersion();
                Debug.Assert(configVersion.HasValue);
                if (configVersion == 1)
                    throw new InvalidOperationException(ResourceErrorMessage.InvalidRollbackConfigOperation);

                //check is requested version is valid
                if (requestedConfigVersion.HasValue)
                {
                    if (requestedConfigVersion <= 0 || configVersion <= requestedConfigVersion)
                        throw new ArgumentOutOfRangeException("requestedConfigVersion",
                            ResourceErrorMessage.InvalidAppVersion);
                }

                //rollback app
                DataBase.RollBackConfig(requestedConfigVersion);

                //get version
                var updatedConfigVersion = DataBase.GetConfigVersion();
                Debug.Assert(updatedConfigVersion.HasValue);
                result = updatedConfigVersion.Value;

                if (requestedConfigVersion.HasValue)
                    Debug.Assert(updatedConfigVersion == requestedConfigVersion);
                else
                    Debug.Assert(updatedConfigVersion == configVersion -1 );

            }
            catch (SqlException e)
            {
                throw new System.InvalidOperationException(ResourceErrorMessage.InternalDBError, e);
            }

            return result;
        }
    }
}
